﻿namespace LivePatterns
{
	using System;
	using System.Linq;
	using System.Reflection;
	using System.Reflection.Emit;

	public static partial class Build
	{
		/// <summary>
		/// Implements <see cref="INotifyMemberAccess"/> and reports every method invocation, including getting/setting property values and adding/removing event handlers.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="obj">The object to create the proxy from.</param>
		/// <returns>Returns a proxy object that implements <see cref="INotifyMemberAccess"/>.</returns>
		/// <example>
		/// IAccount account = session.Account;
		/// 
		/// IAccount auditableAccount = Build.Audit&lt;IAccount&gt;(account);
		/// INotifyMemberAccessed auditable = auditableAccount as INotifyMemberAccessed;
		/// auditable.MemberAccessed += new EventHandler&lt;MemberAccessedEventArgs&gt;(OnMemberAccessed);
		/// 
		/// auditableAccount.Balance += 10; // will raise first get and then set
		/// auditableAccount.Suspend(); // will raise Suspend
		/// </example>
		public static T Audit<T>(T obj) where T : class
		{
			if (typeof(T).GetInterfaces().Contains(typeof(INotifyMemberAccessed)))
				return obj;

			if (!typeof(T).IsInterface)
				throw new ArgumentException("Only interface types are supported.");

			if (obj == null)
				throw new ArgumentNullException("obj");

			TypeBuilder typeBuilder = moduleBuilder.DefineType(String.Format("{0}_AuditProxy_{1}", typeof(T).Name, Guid.NewGuid().ToString().Replace("-", "")), TypeAttributes.Class | TypeAttributes.Public);
			typeBuilder.AddInterfaceImplementation(typeof(T));
			typeBuilder.AddInterfaceImplementation(typeof(INotifyMemberAccessed));

			// create a field for storing the real object instance
			FieldBuilder inner = typeBuilder.DefineField("innerObject", typeof(T), FieldAttributes.Family);

			// create a construtor, in which we can pass the real object instance
			ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(T) });
			ILGenerator cilg = constructorBuilder.GetILGenerator();
			cilg.ConstructObject();
			cilg.Emit(OpCodes.Ldarg_0);
			cilg.Emit(OpCodes.Ldarg_1);
			cilg.Emit(OpCodes.Stfld, inner);

			// define backing field
			FieldBuilder fieldBuilder = null;
			MethodBuilder methodBuilder;
			ILGenerator ilGenerator;

			Action<ILGenerator> writeNotifyMemberAccessed = il =>
			{
				il.Emit(OpCodes.Ldarg_0);
				il.Emit(OpCodes.Ldfld, fieldBuilder);
				il.Emit(OpCodes.Call, typeof(MethodBase).GetMethod("GetCurrentMethod"));
				il.Emit(OpCodes.Newobj, typeof(MemberAccessedEventArgs).GetConstructor(new Type[] { typeof(MethodBase) }));
				il.Emit(OpCodes.Callvirt, typeof(EventHandler<MemberAccessedEventArgs>).GetMethod("Invoke"));
			};

			// implement INMA interface
			{
				EventInfo inpc = typeof(INotifyMemberAccessed).GetEvent("MemberAccessed");

				// define event
				EventBuilder eb = typeBuilder.DefineEvent("MemberAccessed", EventAttributes.None, inpc.EventHandlerType);
				fieldBuilder = typeBuilder.DefineField(String.Format("MemberAccessed"), inpc.EventHandlerType, FieldAttributes.Private);

				//
				// implement add method
				//
				MethodInfo addMethod = inpc.GetAddMethod();
				methodBuilder = typeBuilder.DefineMethod(
					addMethod.Name, _methodAttributes, addMethod.ReturnType, addMethod.GetParameters().Select(p => p.ParameterType).ToArray()
				);

				ilGenerator = methodBuilder.GetILGenerator();
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
				ilGenerator.Emit(OpCodes.Ldarg_1);
				ilGenerator.Emit(OpCodes.Call, typeof(Delegate).GetMethod("Combine", new Type[] { typeof(Delegate), typeof(Delegate) }));
				ilGenerator.Emit(OpCodes.Castclass, typeof(EventHandler<MemberAccessedEventArgs>));
				ilGenerator.Emit(OpCodes.Stfld, fieldBuilder);
				ilGenerator.Return();
				eb.SetAddOnMethod(methodBuilder);

				//
				// implement remove method
				//
				MethodInfo removeMethod = inpc.GetRemoveMethod();
				methodBuilder = typeBuilder.DefineMethod(
					removeMethod.Name, _methodAttributes, removeMethod.ReturnType, removeMethod.GetParameters().Select(p => p.ParameterType).ToArray()
				);

				ilGenerator = methodBuilder.GetILGenerator();
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
				ilGenerator.Emit(OpCodes.Ldarg_1);
				ilGenerator.Emit(OpCodes.Call, typeof(Delegate).GetMethod("Remove", new Type[] { typeof(Delegate), typeof(Delegate) }));
				ilGenerator.Emit(OpCodes.Castclass, typeof(EventHandler<MemberAccessedEventArgs>));
				ilGenerator.Emit(OpCodes.Stfld, fieldBuilder);
				ilGenerator.Return();
				eb.SetRemoveOnMethod(methodBuilder);
			}

			// implement fields
			foreach (FieldInfo fi in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance).Where(m => !m.IsSpecialName))
			{
				FieldBuilder fb = typeBuilder.DefineField(fi.Name, fi.FieldType, FieldAttributes.Public);

				// build constructor
				cilg.Emit(OpCodes.Ldarg_0);
				cilg.Emit(OpCodes.Ldarg_1);
				cilg.Emit(OpCodes.Ldfld, fi);
				cilg.Emit(OpCodes.Stfld, fb);
			}

			cilg.Emit(OpCodes.Ret);

			// implement properties
			foreach (PropertyInfo pi in typeof(T).GetAllVisibleProperties())
			{
				// define property
				PropertyBuilder pb = typeBuilder.DefineProperty(pi.Name, PropertyAttributes.None, typeof(int), Type.EmptyTypes);

				MethodInfo getMethod = pi.GetGetMethod();

				//
				// implement get method
				//
				if (pi.CanRead)
				{
					methodBuilder = typeBuilder.DefineMethod(
						getMethod.Name, _methodAttributes, pi.PropertyType, getMethod.GetParameters().Select(p => p.ParameterType).ToArray()
					);

					ilGenerator = methodBuilder.GetILGenerator();
					writeNotifyMemberAccessed(ilGenerator);
					ilGenerator.Emit(OpCodes.Ldarg_0);
					ilGenerator.Emit(OpCodes.Ldfld, inner);
					ilGenerator.Emit(OpCodes.Call, getMethod);
					ilGenerator.Return();
					pb.SetGetMethod(methodBuilder);
				}

				//
				// implement set method
				//
				if (pi.CanWrite)
				{
					MethodInfo setMethod = pi.GetSetMethod();
					methodBuilder = typeBuilder.DefineMethod(
						setMethod.Name, _methodAttributes, setMethod.ReturnType, setMethod.GetParameters().Select(p => p.ParameterType).ToArray()
					);
					methodBuilder.DefineParameter(1, ParameterAttributes.None, "value");

					ilGenerator = methodBuilder.GetILGenerator();
					writeNotifyMemberAccessed(ilGenerator);
					ilGenerator.Emit(OpCodes.Ldarg_0);
					ilGenerator.Emit(OpCodes.Ldfld, inner);
					ilGenerator.Emit(OpCodes.Ldarg_1);
					ilGenerator.Emit(OpCodes.Call, setMethod);
					ilGenerator.Return();
					pb.SetSetMethod(methodBuilder);
				}
			}

			// implement methods
			foreach (MethodInfo mi in typeof(T).GetAllVisibleMethods())
			{
				methodBuilder = typeBuilder.DefineMethod(
					mi.Name, MethodAttributes.Public | MethodAttributes.Virtual, mi.ReturnType, mi.GetParameters().Select(p => p.ParameterType).ToArray()
				);

				ilGenerator = methodBuilder.GetILGenerator();
				writeNotifyMemberAccessed(ilGenerator);
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldfld, inner);
				ilGenerator.LoadAllArguments(mi);
				ilGenerator.Emit(OpCodes.Callvirt, mi);
				ilGenerator.Return();
			}

			// implement events
			foreach (EventInfo ei in typeof(T).GetAllVisibleEvents())
			{
				// define event
				EventBuilder eb = typeBuilder.DefineEvent(ei.Name, EventAttributes.None, ei.EventHandlerType);

				//
				// implement add method
				//
				MethodInfo addMethod = ei.GetAddMethod();
				methodBuilder = typeBuilder.DefineMethod(
					addMethod.Name, _methodAttributes, addMethod.ReturnType, addMethod.GetParameters().Select(p => p.ParameterType).ToArray()
				);

				ilGenerator = methodBuilder.GetILGenerator();
				writeNotifyMemberAccessed(ilGenerator);
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldfld, inner);
				ilGenerator.Emit(OpCodes.Ldarg_1);
				ilGenerator.Emit(OpCodes.Callvirt, addMethod);
				ilGenerator.Return();
				eb.SetAddOnMethod(methodBuilder);

				//
				// implement remove method
				//
				MethodInfo removeMethod = ei.GetRemoveMethod();
				methodBuilder = typeBuilder.DefineMethod(
					removeMethod.Name, _methodAttributes, removeMethod.ReturnType, removeMethod.GetParameters().Select(p => p.ParameterType).ToArray()
				);

				ilGenerator = methodBuilder.GetILGenerator();
				writeNotifyMemberAccessed(ilGenerator);
				ilGenerator.Emit(OpCodes.Ldarg_0);
				ilGenerator.Emit(OpCodes.Ldfld, inner);
				ilGenerator.Emit(OpCodes.Ldarg_1);
				ilGenerator.Emit(OpCodes.Callvirt, removeMethod);
				ilGenerator.Return();
				eb.SetRemoveOnMethod(methodBuilder);
			}

			return Activator.CreateInstance(typeBuilder.CreateType(), obj) as T;
		}
	}
}